package net.spatula.tally_ho.utils.html;

/* Copyright 2006 Nicklas R Johnson
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * Class for stripping unacceptable tags and attributes from a document
 * 
 * @author spatula
 * 
 */

public class HtmlDocumentCleanser {

	private BadTagHandler handler = new BadTagHandler();
	private final Pattern getAttrs = Pattern.compile("(\\w+)\\{([^}]+)\\}.*");


	public HtmlDocumentCleanser() {

	}

	public HtmlDocumentCleanser(List<AllowedTag> tags) {
		for (AllowedTag tag : tags) {
			addTag(tag);
		}
	}
	
	/**
	 * @param tagPatterns -
	 *            a list of semicolon-separated tags with optional
	 *            comma-separated attributes specified in braces; e.g.,
	 *            "p;a{href};img{src,alt}"
	 */
	public HtmlDocumentCleanser(String tagPatterns) {
		String[] tags = tagPatterns.split(";");

		for (String tag : tags) {
			
			Matcher getAttrMatcher = getAttrs.matcher(tag);
			
			AllowedTag allowedTag = null;
			if (getAttrMatcher.matches()) {
				String attrList = getAttrMatcher.group(2);
				tag = getAttrMatcher.group(1);
				String[] attrs = attrList.split(",");
				allowedTag = new AllowedTag(tag, Arrays.asList(attrs));
			} else {
				allowedTag = new AllowedTag(tag, null);
			}
			
			addTag(allowedTag);
		}

	}

	private Map<String, AllowedTag> tags = new HashMap<String, AllowedTag>();

	class BadTagHandler extends DefaultHandler {

		private StringBuffer output = new StringBuffer();

		private int illegalTagCount = 0;

		private int illegalAttributeCount = 0;

		private List<String> errors = new ArrayList<String>();

		private int lineNumber = 1;

		private int fatalErrors = 0;

		private boolean parseComplete = false;

		private String lastElementAdded = "";

		private int bufferSizeAtLastAdd = 0;

		public BadTagHandler() {
			super();
		}

		public String getOutput() {
			return output.toString();
		}

		@Override
		public void startDocument() throws SAXException {
			lineNumber = 1;
			illegalTagCount = 0;
			illegalAttributeCount = 0;
			fatalErrors = 0;
			parseComplete = false;
			lastElementAdded = "";
			bufferSizeAtLastAdd = 0;
			output = new StringBuffer();
			errors = new ArrayList<String>();
		}

		public void startElement(String uri, String name, String qName,
				Attributes atts) throws SAXException {

			AllowedTag tag = tags.get(qName);
			if (tag == null) { // this tag is not allowed
				illegalTagCount++;
				errors.add("Illegal tag \"" + qName + "\" at line "
						+ lineNumber + ".");
				return;
			}
			output.append("<" + qName);

			int attLength = atts.getLength();

			for (int i = 0; i < attLength; i++) {
				String attName = atts.getQName(i);
				if (tag.isAttributeAllowed(attName)) {
					output.append(" ");
					output.append(attName);
					output.append("=");
					output.append("\"");
					output.append(atts.getValue(i));
					output.append("\"");
				} else {
					errors.add("Illegal attribute \"" + attName
							+ "\" in tag \"" + qName + "\" at line "
							+ lineNumber + ".");
					illegalAttributeCount++;
				}
			}

			output.append(">");
			lastElementAdded = qName;
			bufferSizeAtLastAdd = output.length();
		}

		public void characters(char[] ch, int start, int length)
				throws SAXException {
			StringBuffer buffer = new StringBuffer(length);
			for (int i = start, j = 0; j < length; i++, j++) {
				if (ch[i] == '\n') {
					lineNumber++;
				}
				buffer.append(ch[i]);
				output.append(ch[i]);
			}
		}

		public void endElement(String uri, String localName, String qName)
				throws SAXException {

			AllowedTag tag = tags.get(qName);
			if (tag == null) { // this tag is not allowed
				return;
			}

			// handle tags that open and immediately shut, like <br />

			if (lastElementAdded.equals(qName)
					&& output.length() == bufferSizeAtLastAdd) {
				output.replace(bufferSizeAtLastAdd - 1, bufferSizeAtLastAdd,
						" />");
			} else {
				output.append("</" + qName + ">");
			}
		}

		public void error(SAXParseException e) throws SAXException {
			fatalError(e);

		}

		public void fatalError(SAXParseException e) throws SAXException {
			fatalErrors++;
			errors.add("Fatal parse error at or near line " + lineNumber
					+ ", column " + e.getColumnNumber() + ".");
		}

		public void ignorableWhitespace(char[] ch, int start, int length)
				throws SAXException {
			StringBuffer buffer = new StringBuffer(length);
			for (int i = start, j = 0; j < length; i++, j++) {
				if (ch[i] == '\n') {
					lineNumber++;
				}

				buffer.append(ch[i]);
				output.append(ch[i]);
			}
		}

		public void endDocument() throws SAXException {
			parseComplete = true;
		}
	}

	public void addTag(AllowedTag tag) {
		tags.put(tag.getElementName(), tag);
	}

	public String cleanse(String document) throws HtmlDocumentCleanserException {

		try {
			XMLReader xr = XMLReaderFactory.createXMLReader();
			xr.setContentHandler(handler);
			xr.setErrorHandler(handler);
			xr.parse(new InputSource(new StringReader(document)));
		} catch (SAXException e) {
			throw new HtmlDocumentCleanserException(e);
		} catch (IOException e) {
			throw new HtmlDocumentCleanserException(e);
		}

		if (handler.parseComplete) {
			return handler.getOutput();
		}
		return null;
	}

	public int getIllegalTagCount() {
		return handler.illegalTagCount;
	}

	public int getIllegalAttributeCount() {
		return handler.illegalAttributeCount;
	}

	public List<String> getErrors() {
		return handler.errors;
	}

	public int getFatalErrorsCount() {
		return handler.fatalErrors;
	}

	public int getCorrectionsCount() {
		return handler.illegalTagCount + handler.illegalAttributeCount;
	}

}
